package com.pandajs;

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.eclipse.jetty.util.*;

/**
 * Multipart Form Data Filter.
 * <p>
 * This class decodes the multipart/form-data stream sent by a HTML form that uses a file input
 * item.  Any files sent are stored to a temporary file and a File object added to the request
 * as an attribute.  All other values are made available via the normal getParameter API and
 * the setCharacterEncoding mechanism is respected when converting bytes to Strings.
 * <p>
 * If the init parameter "delete" is set to "true", any files created will be deleted when the
 * current request returns.
 */
public class MultiPartFilter implements Filter
{
	private final static String FILES ="org.eclipse.jetty.servlet.MultiPartFilter.files";
	private File tempdir;
	private boolean _deleteFiles;
	private ServletContext _context;
	private int _fileOutputBuffer = 0;
	private long _maxSize = Long.MAX_VALUE;

	/* ------------------------------------------------------------------------------- */
	/**
	 * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
	 */
	public void init(FilterConfig filterConfig) throws ServletException
	{
		tempdir=(File)filterConfig.getServletContext().getAttribute("javax.servlet.context.tempdir");
		_deleteFiles="true".equals(filterConfig.getInitParameter("deleteFiles"));
		String fileOutputBuffer = filterConfig.getInitParameter("fileOutputBuffer");
		if(fileOutputBuffer!=null)
			_fileOutputBuffer = Integer.parseInt(fileOutputBuffer);

		// Added by Pei Xiaoxing, 2011/5/24
		String maxSize = filterConfig.getInitParameter("maxSize");
		if (maxSize != null) {
			_maxSize = Long.valueOf(maxSize);
		}

		_context=filterConfig.getServletContext();
	}

	/* ------------------------------------------------------------------------------- */
	/**
	 * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
	 *	  javax.servlet.ServletResponse, javax.servlet.FilterChain)
	 */
	@SuppressWarnings("unchecked")
	public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)
		throws IOException, ServletException
	{
		HttpServletRequest srequest=(HttpServletRequest)request;
		if(srequest.getContentType()==null||!srequest.getContentType().startsWith("multipart/form-data"))
		{
			chain.doFilter(request,response);
			return;
		}

		// Added by Pei Xiaoxing, 2011/5/24
		boolean tooLarge = false;
		try {
			long size = Long.valueOf(srequest.getHeader("Content-Length"));
			if (size <= 0 || size > _maxSize) {
				throw new Exception("Invalid Request Size:" + size);
			}
		} catch (Exception e) {
			e.printStackTrace();
			tooLarge = true;
		}

		InputStream in = new BufferedInputStream(request.getInputStream());
		String content_type=srequest.getContentType();

		// TODO - handle encodings
		String boundary="--"+QuotedStringTokenizer.unquote(value(content_type.substring(content_type.indexOf("boundary="))).trim());

		byte[] byteBoundary=(boundary+"--").getBytes(StringUtil.__ISO_8859_1);

		MultiMap params = new MultiMap();
		for (Iterator i = request.getParameterMap().entrySet().iterator();i.hasNext();)
		{
			Map.Entry entry=(Map.Entry)i.next();
			Object value=entry.getValue();
			if (value instanceof String[])
				params.addValues(entry.getKey(),(String[])value);
			else
				params.add(entry.getKey(),value);
		}

		try
		{
			// Get first boundary
			byte[] bytes=TypeUtil.readLine(in);
			String line=bytes==null?null:new String(bytes,"UTF-8");
			if(line==null || !line.equals(boundary))
			{
				throw new IOException("Missing initial multi part boundary");
			}

			// Read each part
			boolean lastPart=false;
			String content_disposition=null;
			String content_transfer_encoding=null;

			outer:while(!lastPart)
			{
				while(true)
				{
					// read a line
					bytes=TypeUtil.readLine(in);
					if (bytes==null)
						break outer;

					// If blank line, end of part headers
					if(bytes.length==0)
						break;
					line=new String(bytes,"UTF-8");

					// place part header key and value in map
					int c=line.indexOf(':',0);
					if(c>0)
					{
						String key=line.substring(0,c).trim().toLowerCase();
						String value=line.substring(c+1,line.length()).trim();
						if(key.equals("content-disposition"))
							content_disposition=value;
						else if(key.equals("content-transfer-encoding"))
							content_transfer_encoding=value;
					}
				}
				// Extract content-disposition
				boolean form_data=false;
				if(content_disposition==null)
				{
					throw new IOException("Missing content-disposition");
				}

				// Modified by Pei Xiaoxing
				// QuotedStringTokenizer tok=new QuotedStringTokenizer(content_disposition,";");
				QuotedStringTokenizer tok=new QuotedStringTokenizer(content_disposition,";", false, true);
				String name=null;
				String filename=null;
				while(tok.hasMoreTokens())
				{
					String t=tok.nextToken().trim();
					String tl=t.toLowerCase();
					if(t.startsWith("form-data"))
						form_data=true;
					else if(tl.startsWith("name="))
						// Modified by Pei Xiaoxing
						// name=value(t);
						name = unquotedValue(t);
					else if(tl.startsWith("filename="))
						// Modified by Pei Xiaoxing
						// filename=value(t);
						filename = unquotedValue(t);
				}

				// Check disposition
				if(!form_data)
				{
					continue;
				}
				//It is valid for reset and submit buttons to have an empty name.
				//If no name is supplied, the browser skips sending the info for that field.
				//However, if you supply the empty string as the name, the browser sends the
				//field, with name as the empty string. So, only continue this loop if we
				//have not yet seen a name field.
				if(name==null)
				{
					continue;
				}

				OutputStream out=null;
				File file=null;
				try
				{
					if (filename!=null && filename.length()>0)
					{
						if (tooLarge) {
							break outer;
						}

						file = File.createTempFile("MultiPart", "", tempdir);
						out = new FileOutputStream(file);
						if(_fileOutputBuffer>0)
							out = new BufferedOutputStream(out, _fileOutputBuffer);
						request.setAttribute(name,file);
						params.add(name, filename);

						if (_deleteFiles)
						{
							file.deleteOnExit();
							// Removed by Pei Xiaoxing
							/*
							ArrayList files = (ArrayList)request.getAttribute(FILES);
							if (files==null)
							{
								files=new ArrayList();
								request.setAttribute(FILES,files);
							}
							files.add(file);
							*/
						}
					}
					else
					{
						out=new ByteArrayOutputStream();
					}


					if ("base64".equalsIgnoreCase(content_transfer_encoding))
					{
						in = new Base64InputStream(in);
					}
					else if ("quoted-printable".equalsIgnoreCase(content_transfer_encoding))
					{
						in = new FilterInputStream(in)
						{
							@Override
							public int read() throws IOException
							{
								int c = in.read();
								if (c >= 0 && c == '=')
								{
									int hi = in.read();
									int lo = in.read();
									if (hi < 0 || lo < 0)
									{
										throw new IOException("Unexpected end to quoted-printable byte");
									}
									char[] chars = new char[] { (char)hi, (char)lo };
									c = Integer.parseInt(new String(chars),16);
								}
								return c;
							}
						};
					}

					int state=-2;
					int c;
					boolean cr=false;
					boolean lf=false;

					// loop for all lines`
					while(true)
					{
						int b=0;
						while((c=(state!=-2)?state:in.read())!=-1)
						{
							state=-2;
							// look for CR and/or LF
							if(c==13||c==10)
							{
								if(c==13)
									state=in.read();
								break;
							}
							// look for boundary
							if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b])
								b++;
							else
							{
								// this is not a boundary
								if(cr)
									out.write(13);
								if(lf)
									out.write(10);
								cr=lf=false;
								if(b>0)
									out.write(byteBoundary,0,b);
								b=-1;
								out.write(c);
							}
						}
						// check partial boundary
						if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1))
						{
							if(cr)
								out.write(13);
							if(lf)
								out.write(10);
							cr=lf=false;
							out.write(byteBoundary,0,b);
							b=-1;
						}
						// boundary match
						if(b>0||c==-1)
						{
							if(b==byteBoundary.length)
								lastPart=true;
							if(state==10)
								state=-2;
							break;
						}
						// handle CR LF
						if(cr)
							out.write(13);
						if(lf)
							out.write(10);
						cr=(c==13);
						lf=(c==10||state==10);
						if(state==10)
							state=-2;
					}
				}
				finally
				{
					// Added by Pei Xiaoxing, 2011/5/24
					if (out != null)
						out.close();
				}

				if (file==null)
				{
					bytes = ((ByteArrayOutputStream)out).toByteArray();
					params.add(name,bytes);
				}
			}

			// handle request
			chain.doFilter(new Wrapper(srequest,params),response);
		}
		finally
		{
			deleteFiles(request);
		}
	}

	@SuppressWarnings("unchecked")
	private void deleteFiles(ServletRequest request)
	{
		ArrayList files = (ArrayList)request.getAttribute(FILES);
		if (files!=null)
		{
			Iterator iter = files.iterator();
			while (iter.hasNext())
			{
				File file=(File)iter.next();
				try
				{
					file.delete();
				}
				catch(Exception e)
				{
					_context.log("failed to delete "+file,e);
				}
			}
		}
	}

	/* ------------------------------------------------------------ */
	private String value(String nameEqualsValue)
	{
		return nameEqualsValue.substring(nameEqualsValue.indexOf('=')+1).trim();
	}

	// Added by Pei Xiaoxing
	private String unquotedValue(String nameEqualsValue)
	{
		return nameEqualsValue.substring(
				nameEqualsValue.indexOf('=') + 2,
				nameEqualsValue.length() - 1
			).trim();
	}

	/* ------------------------------------------------------------------------------- */
	/**
	 * @see javax.servlet.Filter#destroy()
	 */
	public void destroy()
	{
	}

	/* ------------------------------------------------------------------------------- */
	/* ------------------------------------------------------------------------------- */
	@SuppressWarnings("unchecked")
	private static class Wrapper extends HttpServletRequestWrapper
	{
		String _encoding=StringUtil.__UTF8;
		MultiMap _params;

		/* ------------------------------------------------------------------------------- */
		/** Constructor.
		 * @param request
		 */
		public Wrapper(HttpServletRequest request, MultiMap map)
		{
			super(request);
			this._params=map;
		}

		/* ------------------------------------------------------------------------------- */
		/**
		 * @see javax.servlet.ServletRequest#getContentLength()
		 */
		@Override
		public int getContentLength()
		{
			return 0;
		}

		/* ------------------------------------------------------------------------------- */
		/**
		 * @see javax.servlet.ServletRequest#getParameter(java.lang.String)
		 */
		@Override
		public String getParameter(String name)
		{
			Object o=_params.get(name);
			if (!(o instanceof byte[]) && LazyList.size(o)>0)
				o=LazyList.get(o,0);

			if (o instanceof byte[])
			{
				try
				{
					String s=new String((byte[])o,_encoding);
					return s;
				}
				catch(Exception e)
				{
					e.printStackTrace();
				}
			}
			else if (o!=null)
				return String.valueOf(o);
			return null;
		}

		/* ------------------------------------------------------------------------------- */
		/**
		 * @see javax.servlet.ServletRequest#getParameterMap()
		 */
		@Override
		public Map getParameterMap()
		{
			return Collections.unmodifiableMap(_params.toStringArrayMap());
		}

		/* ------------------------------------------------------------------------------- */
		/**
		 * @see javax.servlet.ServletRequest#getParameterNames()
		 */
		@Override
		public Enumeration getParameterNames()
		{
			return Collections.enumeration(_params.keySet());
		}

		/* ------------------------------------------------------------------------------- */
		/**
		 * @see javax.servlet.ServletRequest#getParameterValues(java.lang.String)
		 */
		@Override
		public String[] getParameterValues(String name)
		{
			List l=_params.getValues(name);
			if (l==null || l.size()==0)
				return new String[0];
			String[] v = new String[l.size()];
			for (int i=0;i<l.size();i++)
			{
				Object o=l.get(i);
				if (o instanceof byte[])
				{
					try
					{
						v[i]=new String((byte[])o,_encoding);
					}
					catch(Exception e)
					{
						e.printStackTrace();
					}
				}
				else if (o instanceof String)
					v[i]=(String)o;
			}
			return v;
		}

		/* ------------------------------------------------------------------------------- */
		/**
		 * @see javax.servlet.ServletRequest#setCharacterEncoding(java.lang.String)
		 */
		@Override
		public void setCharacterEncoding(String enc)
			throws UnsupportedEncodingException
		{
			_encoding=enc;
		}
	}

	private static class Base64InputStream extends InputStream
	{
		BufferedReader _in;
		String _line;
		byte[] _buffer;
		int _pos;

		public Base64InputStream (InputStream in)
		{
			_in = new BufferedReader(new InputStreamReader(in));
		}

		@Override
		public int read() throws IOException
		{
			if (_buffer==null || _pos>= _buffer.length)
			{
				_line = _in.readLine();
				if (_line==null)
					return -1;
				if (_line.startsWith("--"))
					_buffer=(_line+"\r\n").getBytes();
				else if (_line.length()==0)
					_buffer="\r\n".getBytes();
				else
					_buffer=B64Code.decode(_line);

				_pos=0;
			}
			return _buffer[_pos++];
		}
	}
}
